package cn.lg.soar.general.service;

import cn.lg.soar.common.algorithm.CryptoUtils;
import cn.lg.soar.common.exceptions.BaseCException;
import cn.lg.soar.common.util.AssertUtil;
import cn.lg.soar.common.util.DatetimeUtil;
import cn.lg.soar.common.util.FileUtil;
import cn.lg.soar.common.util.data.DataUtil;
import cn.lg.soar.common.util.data.RandomUtil;
import cn.lg.soar.general.api.model.FileDigestVO;
import cn.lg.soar.general.api.model.FileVO;
import cn.lg.soar.general.config.FileProps;
import cn.lg.soar.general.entity.FileMetadata;
import cn.lg.soar.general.entity.FileStorage;
import cn.lg.soar.general.manager.FileTemplateManager;
import cn.lg.soar.general.s3.FileTemplate;
import com.amazonaws.SdkClientException;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author luguoxiang 469599794@qq.com
 * @Date: 2024/6/1 13:17
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
public class FileUploadService {

    private static final Logger LOGGER = LoggerFactory.getLogger(FileUploadService.class);

    @Autowired
    private IFileStorageService fileStorageService;
    @Autowired
    private IFileMetadataService fileMetadataService;
    @Autowired
    private FileAccessService fileAccessService;
    @Autowired
    private FileTemplateManager uploadTemplateManager;
    @Autowired
    private FileProps props;

    @Transactional(rollbackFor = Exception.class)
    public List<FileMetadata> upload(String bucketName, Collection<MultipartFile> files) {
        // 获取默认仓库
        FileStorage defaulted = fileStorageService.getDefaulted();
        AssertUtil.notNull(defaulted, "未找到默认文件仓库");
        FileTemplate uploadTemplate = uploadTemplateManager.getTemplate(defaulted);
        Long storageId = defaulted.getId();
        // 处理文件
        List<FileMetadata> addList = new ArrayList<>(files.size());
        List<FileMetadata> updateList = new ArrayList<>(files.size());
        List<FileMetadata> metadataList = new ArrayList<>(files.size());
        for (MultipartFile file : files) {
            String originalFilename = file.getOriginalFilename();
            long size = file.getSize();
            File tempFile = null;
            InputStream inputStream = null;
            try {
                // 文件基本信息及校验
                int index = originalFilename.lastIndexOf(".");
                AssertUtil.isTrue(index != -1, "未能识别文件扩展名");
                String suffix = originalFilename.substring(index + 1);
                AssertUtil.isFalse(props.isBlacklist(suffix), "文件【{0}】类型不允许上传，在黑名单内", originalFilename);
                AssertUtil.isTrue(props.isWhitelist(suffix), "文件【{0}】类型不允许上传，不在白名单内", originalFilename);

                String md5;
                // 大于5M的文件做特殊处理
                if (size > 5242880) {
                    // 保存临时文件
                    tempFile = File.createTempFile("lg-soar-file", UUID.randomUUID().toString());
                    file.transferTo(tempFile);
                    inputStream = Files.newInputStream(tempFile.toPath());
                    // md5处理
                    MessageDigest digest = MessageDigest.getInstance("MD5");
                    byte[] buffer = new byte[8192];
                    int bytesRead;
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        digest.update(buffer, 0, bytesRead);
                    }
                    md5 = DataUtil.byteToHex(digest.digest());
                    // 重新生成流
                    inputStream.close();
                    inputStream = Files.newInputStream(tempFile.toPath());
                } else {
                    byte[] bytes = file.getBytes();
                    md5 = CryptoUtils.md5Hex(bytes);
                    inputStream = new ByteArrayInputStream(bytes);
                }

                // 生成code
                String code = FileUtil.toCode(md5, size);
                // 查找文件信息是否已存在
                FileMetadata metadata = fileMetadataService.getByCode(code);
                if (metadata == null) {
                    // 文件信息不存在
                    metadata = new FileMetadata();
                    metadata.setId(IdWorker.getId());
                    metadata.setOriginalName(originalFilename);
                    // 新增
                    addList.add(metadata);
                } else {
                    // 文件信息已存在
                    metadata.setOriginalName(originalFilename);
                    try {
                        // 判断文件是否存在
                        FileTemplate uploadTemplate1 = uploadTemplateManager.getTemplate(metadata.getStorageId());
                        if (uploadTemplate1 != null && uploadTemplate1.isExist(metadata.getBucketName(), metadata.filename())) {
                            metadataList.add(metadata);
                            continue;
                        }
                    } catch (RuntimeException ignored) {
                        LOGGER.error("检查文件是否存在异常", ignored);
                    }
                    // 文件已不存在，需要重新上传
                    updateList.add(metadata);
                }
                // 记录文件元信息
                metadata.setCode(code);
                metadata.setStorageId(storageId);
                metadata.setBucketName(bucketName);
                metadata.setSuffix(suffix);
                metadata.setSize((int) (size & 0xFFFFFFFFL));
                metadata.setSecret(RandomUtil.getString(6, RandomUtil.SOURCE64));
                // 上传文件
                uploadTemplate.upload(bucketName, metadata.filename(), inputStream);
                metadataList.add(metadata);
            } catch (SdkClientException e) {
                LOGGER.error("连接文件仓库失败", e);
                throw new BaseCException(3, "连接文件仓库失败");
            } catch (Exception e) {
                LOGGER.error("文件上传异常", e);
                FileMetadata metadata = new FileMetadata();
                metadata.setOriginalName(originalFilename);
                metadata.setSize((int) size);
                metadataList.add(metadata);
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        LOGGER.error("关闭流失败", e);
                    }
                }
                if (tempFile != null) {
                    try {
                        tempFile.delete();
                    } catch (Exception e) {
                        LOGGER.error("关闭临时文件", e);
                    }
                }
            }
        }
        if (DataUtil.isValuable(addList)) {
            addList = addList.stream()
                    .distinct()
                    .collect(Collectors.toList());
            AssertUtil.isTrue(fileMetadataService.saveBatch(addList), "文件上传失败");
        }
        if (DataUtil.isValuable(updateList)) {
            AssertUtil.isTrue(fileMetadataService.updateBatchById(updateList), "文件上传失败");
        }
        // 返回数据
        return metadataList;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<FileMetadata> upload(String bucketName, MultipartFile... files) {
        return upload(bucketName, Arrays.asList(files));
    }

    @Transactional(rollbackFor = Exception.class)
    public List<FileMetadata> upload(Collection<MultipartFile> files) {
        return upload(getOrCreateBucket(), files);
    }

    @Transactional(rollbackFor = Exception.class)
    public List<FileMetadata> upload(MultipartFile... files) {
        return upload(getOrCreateBucket(), Arrays.asList(files));
    }

    public List<FileMetadata> fastUpload(Collection<FileDigestVO> fileDigestVos) {
        List<FileMetadata> metadataList = new ArrayList<>(fileDigestVos.size());
        for (FileDigestVO vo : fileDigestVos) {
            String code = FileUtil.toCode(vo.getDigest(), vo.getSize());
            FileMetadata metadata = fileMetadataService.getByCode(code);
            if (metadata == null) {
                continue;
            }
            // 判断文件是否存在
            try {
                FileTemplate uploadTemplate = uploadTemplateManager.getTemplate(metadata.getStorageId());
                if (uploadTemplate.isExist(metadata.getBucketName(), metadata.filename())) {
                    metadata.setOriginalName(vo.getName());
                    metadataList.add(metadata);
                }
            } catch (RuntimeException e) {
                LOGGER.error("检查文件是否存在异常", e);
            }
        }
        return metadataList;
    }

    public List<FileMetadata> fastUpload(FileDigestVO... fileDigestVos) {
        return fastUpload(Arrays.asList(fileDigestVos));
    }

    public FileVO toVo(FileMetadata metadata) {
        FileVO fileVo = new FileVO();
        fileVo.setId(metadata.getId());
        fileVo.setName(metadata.getOriginalName());
        fileVo.setSize(metadata.getSize());
        fileVo.setUrl(fileAccessService.createFixedSecretUrl(metadata));
        return fileVo;
    }

    public List<FileVO> toVos(List<FileMetadata> metadataList) {
        return metadataList.stream().map(this::toVo).collect(Collectors.toList());
    }

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMM");
    private String bucketName;
    private long expires;
    private String getOrCreateBucket() {
        if (DataUtil.isValuable(bucketName) && expires > System.currentTimeMillis()) {
            return bucketName;
        }
        FileTemplate uploadTemplate = uploadTemplateManager.getTemplateByDetaulted();
        // 创建桶
        bucketName = LocalDate.now().format(FORMATTER);
        uploadTemplate.createBucket(bucketName);
        LocalDateTime now = LocalDateTime.now()
                .withNano(0)
                .withSecond(0)
                .withMinute(0)
                .withHour(0)
                .plusDays(1);
        expires = DatetimeUtil.timestamp(now);
        return bucketName;
    }

}
